package com.acme.bankapp.service.bank;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
import java.util.Set;

import com.acme.bankapp.domain.bank.*;
import com.acme.bankapp.domain.bank.exceptions.ClientAlreadyExistsException;
import com.acme.bankapp.domain.bank.exceptions.NotEnoughFundsException;

/**
 * @author Artem Kalchenko
 * 
 */
public class BankService {

	static final String CLIENT_EXISTS_ERROR_MSG = "Client already exists";
	static final String CLIENT_NOT_IN_BANK_ERROR_MSG = "Not a bank's client or the account doesn't belong to the client";
	static final String NOT_ENOUGH_FUNDS_ERROR_MSG = "The balance is insufficient to withdraw ";
	static final String ILLEGAL_AMOUNT_ERROR_MSG = "Illegal amount ";
	static final String COULDNT_SAVE_FILE_ERROR_MSG = "Couldn't save the bank state";
	static final String CLASS_READ_ERROR_MSG = "Class read error";

	static final String BANK_SERIAL_FILE_NAME = "bank.ser";

	/**
	 * Add a new client to a bank
	 * 
	 * @param bank
	 * @param client
	 * @return client
	 */
	public Client addClient(Bank bank, Client client) {
		try {
			bank.addClient(client);
			this.saveBank(bank);
			return client;
		} catch (ClientAlreadyExistsException e) {
			System.out.println(CLIENT_EXISTS_ERROR_MSG);
			return null;
		}
	}

	/**
	 * Add a new client to a bank
	 * 
	 * @param bank
	 * @param name
	 * @param gender
	 * @return client object
	 */
	public Client addClient(Bank bank, String name, Gender gender) {
		Client client = new Client(name, gender);

		client = this.addClient(bank, client);
		this.saveBank(bank);
		return client;
	}

	/**
	 * Get a client list
	 * 
	 * @param bank
	 * @param client
	 */

	public Set<Client> getClientsList(Bank bank) {
		return bank.getClients();
	}

	/**
	 * Add an account to a given client
	 * @param bank TODO
	 * @param client
	 * @param account
	 */
	public void addAccount(Bank bank, Client client, Account account) {
		if (bank.getClients().contains(client)) {
			client.addAccount(account);
			this.saveBank(bank);
		} else {
			System.out.println(CLIENT_NOT_IN_BANK_ERROR_MSG);
		}
	}

	/**
	 * Report the list of bank's clients
	 * 
	 * @param bank
	 * @return list of client names on separate lines
	 */
	public String getClientsListAsString(Bank bank) {
		StringBuffer buff = new StringBuffer();

		for (Client cl : bank.getClients()) {
			buff.append(cl.getClientSalutation());
			buff.append("\n");
		}
		return buff.toString();
	}

	/**
	 * Report the state of client's accounts
	 * 
	 * @param bank
	 * @return list of client's accounts with balance on separate lines
	 */
	public String getClientAccount(Client client) {
		StringBuffer buff = new StringBuffer();

		for (Account acc : client.getAccounts()) {
			buff.append(acc.toString());
			buff.append("\n");
		}
		return buff.toString();
	}

	/**
	 * Report the list of bank's accounts
	 * 
	 * @param bank
	 * @return list of client names with their accounts on separate lines
	 */
	public String getAccountsListAsString(Bank bank) {
		StringBuffer buff = new StringBuffer();

		for (Client cl : bank.getClients()) {
			buff.append(cl.toString());
			buff.append(":\n");
		}
		return buff.toString();
	}

	/**
	 * Withdraw money from client's account
	 * @param bank 
	 * @param client
	 * @param amount
	 * 
	 * @return true if money successfully withdrawn, false if not enough money
	 */
	public void withdraw(Bank bank, Client client, Account account, double amount) {
		if (bank.getClients().contains(client) && client.getAccounts().contains(account)) {
			try {
				account.withdraw(amount);
				this.saveBank(bank);
			} catch (NotEnoughFundsException e) {
				System.out.println(NOT_ENOUGH_FUNDS_ERROR_MSG + amount);
			} catch (IllegalArgumentException e) {
				System.out.println(ILLEGAL_AMOUNT_ERROR_MSG);
			}
		} else {
			System.out.println(CLIENT_NOT_IN_BANK_ERROR_MSG);
		}
	}

	/**
	 * Deposit money on client's account
	 * @param bank TODO
	 * @param client
	 * @param amount
	 */
	public void deposit(Bank bank, Client client, Account account, double amount) {
		if (bank.getClients().contains(client) && client.getAccounts().contains(account)) {
			try {
				account.deposit(amount);
				this.saveBank(bank);
			} catch (IllegalArgumentException e) {
				System.out.println(ILLEGAL_AMOUNT_ERROR_MSG);
			}
		} else {
			System.out.println(CLIENT_NOT_IN_BANK_ERROR_MSG);
		}
	}

	/**
	 * Set an overdraft limit for the client's checking account
	 * @param bank TODO
	 * @param client
	 * @param overdraftLim
	 *            - limit
	 */
	public boolean setOverdraftLimit(Bank bank, Client client, int accNum, double overdraftLim) {
		if (bank.getClients().contains(client)) {
			if (client.getAccount(accNum) instanceof CheckingAccount) {
				((CheckingAccount) client.getAccount(accNum)).setMaxOverdraft(overdraftLim);
				this.saveBank(bank);
				return true;
			} 
		}
		return false;
	}

	public boolean saveBank(Bank bank) {
		try (FileOutputStream fos = new FileOutputStream(BANK_SERIAL_FILE_NAME);
				ObjectOutputStream oos = new ObjectOutputStream(fos)) {
			oos.writeObject(bank);
		} catch (IOException e) {
			System.out.println(COULDNT_SAVE_FILE_ERROR_MSG);
			e.printStackTrace();
			return false;
		}
		return true;
	}

	public Bank loadBank(String fileName) {
		try (FileInputStream fis = new FileInputStream(fileName);
				ObjectInputStream ois = new ObjectInputStream(fis)) {
			try {
				Bank newBank = (Bank) ois.readObject();
				newBank.addDefaultClientRegistrationListeners();
				return newBank;
			} catch (ClassNotFoundException e) {
				System.out.println(CLASS_READ_ERROR_MSG);
				return null;
			}
		} catch (IOException e) {
			System.out.println(CLASS_READ_ERROR_MSG);
			return null;
		}
	}
	
	public Bank loadBank() {
		return this.loadBank(BANK_SERIAL_FILE_NAME);
	}
	
	public Bank loadBankFromFeed(String fileName){
		Bank bank = new Bank();
		
		return BankFeederService.readBank(bank, fileName);
	}
}
